<?php

/**
 * 文件处理
 * User: wz_zh
 * Date: 2017/7/27
 * Time: 13:59
 */

namespace PKFrame\Lib;
defined('PATH_PK') or die();

use Exception;
use PKFrame\DataHandler\Arrays;
use PKFrame\DataHandler\Convert;
use PKFrame\DataHandler\MatchHelper;
use PKFrame\DataHandler\Pages;
use PKFrame\Extend\Fsocketopen;

class Files
{

    private static $instance;

    /**
     * @return Files
     */
    public static function getInstance(): Files
    {
        if (is_null(static::$instance)) {
            static::$instance = new static;
        }

        return self::$instance;
    }

    /**
     * 检查文件中是否带有BOM
     * @param string $filename 被检查的文件名
     */
    public function IsBom($filename = '')
    {
        $contents = file_get_contents($filename);
        $charset[1] = substr($contents, 0, 1);
        $charset[2] = substr($contents, 1, 1);
        $charset[3] = substr($contents, 2, 1);
        if (ord($charset[1]) == 239 && ord($charset[2]) == 187 && ord($charset[3]) == 191) {
            $data = substr($contents, 3);
            $filenum = fopen($filename, "w");
            flock($filenum, LOCK_EX);
            fwrite($filenum, $data);
            fclose($filenum);
        }
    }

    /**
     * 检查要保存的文件名
     *
     * @param string $str
     * @return string
     */
    public function CheckFileName($str = ''): string
    {
        $str = str_replace(' ', '-', $str);
        $str = str_replace('/', '-', $str);
        $str = str_replace(',', '', $str);
        $str = str_replace('&nbsp;', '', $str);
        $str = str_replace(';', '', $str);
        $str = str_replace('&', '', $str);
        $str = urlencode($str);
        //        $str = strtolower($str);
        $str = str_replace('%26nbsp%3b', '', $str);
        $str = str_replace('%c2%a0', '', $str);
        $str = str_replace('%3a', ':', $str);
        return $str;
    }

    /**
     * 获取文件（属性）信息，包含基本属性
     * @param string $path 文件所在位置
     * @return array|mixed
     */
    public function Property($path = ''): array
    {
        $header_info = stristr($path, 'http://') ?
            (!Pages::IsUrl($path) ? $path : get_headers($path))
            : $header_info = file_exists($path) ? $this->baseProperty($path)
                : $path;
        if (Arrays::Is($header_info) == FALSE) {
            return [];
        }
        $info = pathinfo($path);
        $img_type = array('jpg', 'gif', 'png');
        if (in_array($info['extension'], $img_type)) {
            $info = array_merge($info, getimagesize($path));
        }
        if (isset($header_info)) {
            $info = array_merge($info, $header_info);
            $info['size'] = filesize($path);
        }
        if (isset($arr) && Arrays::Is($arr))
            $info = array_merge($info, $arr);
        return $info;
    }

    /**
     * 文件基本属性
     * a_time：上次访问时间，c_time：文件创建时间，m_time：上次修改时间
     * perms：文件权限，size：文件大小，type：文件类型
     * @param string $path
     * @return array
     */
    public function BaseProperty($path = ''): array
    {
        $result = [];
        if (!file_exists($path)) {
            $msg_str = language('path_notExists') . $path;
            if (request()->isAjax()) {
                out()->noticeByJson($msg_str);
            } else {
                out()->notice($msg_str);
            }
        }
        $result['a_time'] = fileatime($path);
        $result['c_time'] = filectime($path);
        $result['m_time'] = filemtime($path);
        $result['perms'] = substr(sprintf("%o", fileperms($path)), -4);
        $result['size'] = Convert::bytesToUnit(filesize($path));
        $result['type'] = filetype($path);
        return $result;
    }

    /**
     * 获取 MIME 类型
     * @param $path_file
     * @return string
     */
    public function GetMIME($path_file): string
    {
//        if (function_exists('mime_content_type')) {
//            return mime_content_type($path_file);
//        } else {
        $info_file = finfo_open(FILEINFO_MIME);
        $type_mime = finfo_file($info_file, $path_file);
        finfo_close($info_file);
        return $type_mime;
//        }
    }

    /**
     * 获取图片 MIME 类型
     * @param $path_img
     * @return string
     */
    public function GetMIMEByImg($path_img): string
    {
        return image_type_to_mime_type($path_img);
    }

    /**
     * check read and write, power
     * @param string $path
     */
    public function RW_Power($path = '')
    {
        try {
            if (!(is_readable($path) && is_writable($path))) {
                throw new \Exception('check path:' . $path . ', read and write power');
            }
        } catch (\Exception $exception) {
            handlerException($exception);
        }
    }

    /**
     * 文件的保存
     * @param string $path 保存路径
     * @param string $fileName 保存文件名
     * @param string $data 保存内容的信息
     * @param bool $isTop true：头部开始，false：尾部开始
     */
    public function PutContents($path = '', $fileName = '', $data = null, $isTop = true)
    {
        $fileName = $this->checkFileName($fileName);
        $path = rtrim($path, DS) . DS;
        $this->MKDir($path);
        if (file_exists($path . $fileName)) {
            $info = $this->property($path . $fileName);
            if ($info['perms'] == 444) {
                $old = umask(0);
                chmod($path . $fileName, 0755);
                umask($old);
            }
        }
        $data = iconv("UTF-8", "UTF-8", $data);
        if ($isTop) {
            return file_put_contents($path . $fileName, $data);
        } else {
            return file_put_contents($path . $fileName, $data, FILE_APPEND);
        }
    }

    public function GetFileExtension($fileName): string
    {
        try {
            if (!file_exists($fileName)) {
                throw new \Exception('file not exists! path:' . $fileName);
            }
        } catch (\Exception $exception) {
            request()->isAjax()
                ? out()->noticeByJson($exception->getMessage())
                : out()->notice($exception->getMessage());
        }
        $Extension = explode(".", $fileName);
        $Extension = $Extension[count($Extension) - 1];
        return strtolower($Extension);
    }

    /**
     * 文件的读取
     * @param string $path_disk
     * @param string|null $str_tips
     * @param string|null $app_lang
     * @return false|string
     */
    public function GetContentsByDisk(string $path_disk, string $str_tips = null, string $app_lang = null)
    {
        $buffer = '';
        if (!file_exists($path_disk) || !is_file($path_disk)) {
            if (empty($str_tips)) {
                return $buffer;
            } else {
                out()->noticeByLJson($str_tips, $app_lang);
            }
        }
        is_readable($path_disk) ?: out()->noticeByJson('file unable read ' . $path_disk);
        if (function_exists('file_get_contents')) {
            $buffer = file_get_contents($path_disk);
        } else {
            $fp = \fopen($path_disk, 'r');
            while (!feof($fp)) {
                $buffer = fgets($fp, 4096);
            }
            fclose($fp);
        }
        return $buffer;
    }

    /**
     * 获取 WEB 页的代码
     * @param string $path_url
     * @param int $HTTP_timeOut
     * HTTP 协议读取的超时设定，0为不设定
     * @param bool $is_remove_HP
     * false：普通读取，true：过滤掉 HTML 和 PHP 标记
     * @return bool|string
     */
    public function GetContentsByHTTP(string $path_url, int $HTTP_timeOut = 0, bool $is_remove_HP = false)
    {
        $buffer = '';
        if (!strpos($path_url, '//')) {
            return $buffer;
        }
        if (!$is_remove_HP && function_exists('fsockopen')) {
            $buffer = Fsocketopen::GetHttpCode($path_url);
        } elseif (!$is_remove_HP && function_exists('file_get_contents')) {
            try {
                if (!get_cfg_var('allow_url_fopen')) {
                    throw new \Exception(language('PHPSetting_allow_url_fopen_NoExists'));
                }
            } catch (\Exception $e) {
                handlerException($e);
            }
            if ($HTTP_timeOut > 0) {
                $buffer = @file_get_contents($path_url, false, stream_context_create(
                    array('http' => array('timeout' => $HTTP_timeOut))
                ));
            } else {
                $buffer = @file_get_contents($path_url, false);
            }
        } else {
            $fp = \fopen($path_url, 'r');
            if ($is_remove_HP) {
                while (!feof($fp)) {
                    $buffer = fgets($fp, 4096);
                }
            } else {
                while (!feof($fp)) {
                    $buffer = fgets($fp, 4096);
                }
            }
            fclose($fp);
        }
        return $buffer;
    }

    /**
     * 保存上传文件
     * @param string $tmp 缓冲文件
     * @param string $path 保存文件的路径
     * @param string $new 保存文件的新文件名
     */
    public function Uploaded(string $tmp, string $path, string $new)
    {
        $new = $this->checkFileName($new);
        $this->MKDir($path);
        set_time_limit(0);
        \move_uploaded_file($tmp, $path . $new) ?: die('uploaded file fail');
    }

    /**
     * 给访客提供文件下载
     * @param string $filePath 文件路径
     */
    public function DownloadByGuest($filePath = '')
    {
        $filename = basename($filePath);
        !strpos(\request()->browserAndVer(), 'ie') ?: $filename = rawurlencode($filename);
        $filetype = strtolower(trim(substr(strrchr($filename, '.'), 1, 10)));
        $filesize = sprintf("%u", filesize($filePath));
        if (ob_get_length() !== false)
            @ob_end_clean();
        header('Pragma: public');
        header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
        header('Tools-Control: no-store, no-cache, must-revalidate');
        header('Tools-Control: pre-check=0, post-check=0, max-age=0');
        header('Content-Transfer-Encoding: binary');
        header('Content-Encoding: none');
        header('Content-type: ' . $filetype);
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        header('Content-length: ' . $filesize);
        readfile($filePath);
        exit();
    }

    /**
     * 下载远程文件到本地
     * @param string $path_local
     * @param string $file_name
     * @param string $path_online
     */
    public function DownLoadOnlineToLocal(string $path_local, string $file_name, string $path_online)
    {
        $path_disk = $path_local . $file_name;
        try {
            fileHelper()->MKDir($path_local);
            if (!MatchHelper::checkUrlOfDownLoad($path_online)) {
                throw new \Exception('Error: URL ' . $path_online . ' invalid.');
            }
            set_time_limit(0);
            touch($path_disk);
            if (!$download_fp = fopen($path_disk, "wb")) {
                throw new \Exception('Error: Open local file ' . $path_disk . 'failed.');
            }
            // 做些日志处理
            if ($fp = fopen($path_online, "rb")) {
                while (!feof($fp)) {
                    if (!file_exists($path_disk)) {
                        // 如果临时文件被删除就取消下载
                        fclose($download_fp);
                        throw new \Exception('The physical storage path cannot be found, the path:' . $path_disk);
                    }
                    fwrite($download_fp, fread($fp, 1024 * 8), 1024 * 8);
                }
                fclose($download_fp);
                fclose($fp);
            } elseif (function_exists('fsockopen')) {
                $urls = parse_url($path_online);
                # 建立连接
                $fp = fsockopen($urls['host'], '80', $err_no, $err_str);
                stream_set_blocking($fp, true);
                if (!$fp) {
                    throw new \Exception($err_str, $err_no);
                } else {
                    # 发送一个HTTP请求信息头
                    $request_header = "GET " . $urls['path'] . " HTTP/1.1" . PHP_EOL;
                    # 起始行
                    # 头域
                    $request_header .= "Host: " . $urls["host"] . PHP_EOL;
                    # 再一个回车换行表示头信息结束
                    $request_header .= PHP_EOL;

                    # 发送请求到服务器
                    fputs($fp, $request_header);

                    # 接受响应
//                    $download_fp = fopen($path_disk, 'w'); // 要下载的文件名  下载到指定目录
                    $line = '';
                    while (!feof($fp)) {
                        $line .= fputs($download_fp, fgets($fp));
                    }
//                    if (feof($fp)) {
////                        echo "<script>alert('已下载到当前目录')</script>";
//                    }
                    # 关闭
                    fclose($download_fp);
                    fclose($fp);
                }

            } else {
                throw new \Exception('Error: Download :' . $path_online . ' failed.');
            }
        } catch (\Exception $e) {
            handlerException($e);
        }
    }

    /** sockopen 下载文件
     * @param string $url 访问文件的url 地址
     * @param int $port 默认 80
     * @param string|null $down_name 下载指定路径文件名称 例如 ../aa.zip
     */
    private function _sockDown(string $url, int $port = 80, string $down_name = null)
    {
        $info = parse_url($url);

    }

    /**
     * 文件复制
     * @param string $source 原文件路径（包括文件所在路径和文件名）
     * @param string $target 要复制到目的的路径
     * @param string $file_name 要复制目的的文件名
     * @return boolean
     */
    public function Copy($source = '', $target = '', $file_name = ''): bool
    {
        if (file_exists($source)) {
            file_exists($target) ?: $this->MKDir($target);
            $file_name = $this->CheckFileName($file_name);
            copy($source, $target . '/' . $file_name);
            return true;
        } else
            return false;
    }

    /**
     * 目录（文件夹）的创建
     * @param string $path 目录（文件夹）的路径
     * @return boolean
     */
    public function MKDir(string $path, string $path_match = null)
    {
        if (empty($path)) {
            return $path;
        }
        if (file_exists($path)) {
            $this->RW_Power($path);
            return $path;
        }
        $path_match = is_null($path_match) ? PATH_ROOT : $path_match;
        $path = str_replace('\\', DS, $path);
        $paths = explode(DS, $path);
        $result = array_shift($paths);
        foreach ($paths as $value) {
            $result .= DS . $this->checkFileName($value);
            try {
                if (!file_exists($result) && strstr($result, $path_match)) {
                    @\MKDir($result, 0777, true) ?: out()->notice('Directory creation failed, path: ' . $result);
                } elseif (file_exists($result) && strstr($result, $path_match)) {
                    $power_code = fileperms($result);
                    // 返回的值不是数字，说明返回目录的权限失败
                    if (is_numeric($power_code)) {
                        // 修改存在的目录的权限
                        $power_code = intval(substr(sprintf("%o", fileperms($result)), -4));
                        if ($power_code < 755) {
                            @chmod($result, 0777) ?: out()->notice('Failed to modify permissions for directory, path: ' . $result);
                        }
                    }
                }
            } catch (\Exception $e) {
                out()->notice('MKDir:' . $result . 'errMsg:' . $e->getMessage());
            }
        }
        return $result . DS;
    }

    /**
     * 复制文件夹
     * @param $source
     * @param $dest
     */
    public function CopyDir($source, $dest)
    {
        if (is_dir($source)) {
            $this->MKDir($dest);
            $files = scandir($source);
            foreach ($files as $file) {
                if ($file != "." && $file != "..") {
                    $this->CopyDir($source . DS . $file, $dest . DS . $file);
                }
            }
        } else if (file_exists($source)) {
            copy($source, $dest);
        }
    }

    public function Rename(string $from, string $to): string
    {
        try {
            if (file_exists($from)) {
                $path_arr = explode(DS, $from);
                is_dir($from) ?: $name_arr = explode('.', end($path_arr));
                array_pop($path_arr);
                $path_new = implode(DS, $path_arr) . DS . $to .
                    (isset($name_arr) ? '.' . $name_arr[count($name_arr) - 1] : '');
                return rename($from, $path_new) ? $path_new : '';
            } else {
                throw new Exception('Rename failed, could not find the path of the file:' . $from);
            }
        } catch (Exception $e) {
            handlerException($e);
        }
        return '';
    }

    public function RemoveDir($dir)
    {
        if (is_dir($dir)) {
            $files = scandir($dir);
            foreach ($files as $file) {
                if ($file != "." && $file != "..") {
                    $this->RemoveDir($dir . DS . $file);
                }
            }
            rmdir($dir);
        } else if (file_exists($dir)) {
            UnLink($dir);
        }
    }

    /**
     * 目录（文件夹）的清空
     * @param string $path 目录（文件夹）的路径
     * @param array $exclude_dir 排除的文件夹
     */
    public function Remove(string $path = '', array $exclude_dir = [])
    {
        if (file_exists($path)) {
            if ($dh = @\opendir($path)) {
                while ($fstr = @\readdir($dh)) {
                    if ($fstr == "." || $fstr == "..") {
                        continue;
                    }
                    $fname = rtrim($path, DS) . DS . $fstr;
                    if (is_dir($fname)) {
                        if (Arrays::Is($exclude_dir) && in_array($fstr, $exclude_dir)) {
                            continue;
                        }
                        \is_writable($fname) ?: die($fname . 'not writable');
                        \chmod($fname, 0666);
                        $this->UnLink($fname);
                    } else {
                        if (file_exists($fname)) {
                            $this->UnLink($fname);
                        }
                    }
                }
            }
        } elseif (file_exists($path)) {
            $this->UnLink($path);
        }
    }

    /**
     * 系统内置删除，可解决 windows 下因权限问题而不能删除的情况
     * @param $fileName
     * @return bool
     */
    public function UnLink($fileName): bool
    {
        if (!file_exists($fileName)) {
            return false;
        }
        try {
            $uri = strpos($fileName, ':\\');
            if (!unlink($fileName)) {
                if (is_numeric($uri) && $uri == 1) {
                    // is windows
                    exec("DEL /F/Q \"$fileName\"", $lines, $deleteError);
                } else {
                    throw new Exception('Drop Failure: ' . $fileName);
                }
            }
        } catch (\Exception $exception) {
            handlerException($exception);
        }
        return true;
    }

    /**
     * 目录（文件夹）的浏览
     * @param string $folder 目录（文件夹）
     * @param null $ext
     * @param string|array $search 查找的文件名
     * @param bool $isResultChildren
     * @return array
     */
    public function OpenFolder(string $folder = '', $ext = null, $search = null, bool $isResultChildren = false): array
    {
        $result = [];
        if (stristr($folder, 'ftp://')) {
            $headdir = @\opendir($folder) or die("Cannot open " . $folder);
            while (($file = opendir($headdir)) !== false) {
                $result[] = $file;
            }
            closedir($headdir);
        } else {
            $result = $this->folderChildren($folder, $ext, $search, $isResultChildren);
        }
        return $result;
    }

    /**
     * 遍历子目录
     * @param string $path 要遍历的目录
     * @param null $ext
     * @param string|array $search 要查找的文件名
     * @param bool $isResultChildren
     * @return array
     */
    private function folderChildren(string $path = '', $ext = null, $search = null, bool $isResultChildren = false): array
    {
        $headDir = @dir($path);
        $result = [];
        if (!method_exists($headDir, 'read')) {
            return $result;
        }
        while (($file = $headDir->read()) != FALSE) {
            $tmp_path = rtrim($path, DS) . DS . $file;
            if (is_dir($tmp_path) && is_null($ext) && is_null($search)) {
                if (!stristr($file, '.')) {
                    $result[] = $file;
                }
            } elseif (is_file($tmp_path)) {
                $fileExt = $this->GetFileExtension($path . DS . $file);
                $file_property = $this->BaseProperty($path . DS . $file);
                if (is_string($ext) && $fileExt == $ext) {
                    $result[] = array_merge([
                        'file_name' => $file, 'file_ext' => $fileExt,
                        'file_path' => urlencode(str_replace(DS, '/', $path . DS . $file))
                    ], $file_property);
                } elseif (is_array($ext) && in_array($fileExt, $ext)) {
                    $result[] = array_merge([
                        'file_name' => $file, 'file_ext' => $fileExt,
                        'file_path' => urlencode(str_replace(DS, '/', $path . DS . $file))
                    ], $file_property);
                } elseif (is_string($search) && stristr($file, $search)) {
                    $result[] = array_merge([
                        'file_name' => $file, 'file_ext' => $fileExt,
                        'file_path' => urlencode(str_replace(DS, '/', $path . DS . $file))
                    ], $file_property);
                } elseif (is_null($fileExt) && is_null($search)) {
                    $result[] = str_replace(DS, '/', $path . DS . $file);
                }
            }
            if (is_dir($tmp_path) && $isResultChildren) {
                $lists = $this->folderChildren($path . '/' . $file, $search);
                if (!empty($lists))
                    $result[$file] = $lists;
            }
        }
        $headDir->close();
        return $result;
    }
}
